#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import sys
from enum import Enum, unique
import xml.etree.ElementTree as ET
if sys.version_info.major == 2:
    import io

OPEN = open
if sys.version_info.major == 2:
    OPEN = io.open
    import sys
    reload(sys)
    sys.setdefaultencoding('utf8')

@unique
class FieldType(Enum):
    DATETIME = 'datetime'
    INT      = 'int'
    INT64    = 'int64'
    BIGINT   = 'bigint'
    UINT64   = 'uint64'
    STRING   = 'string'
    FLOAT    = 'float'
    def __init__(self, field_type):
        self.__MAX_INT = 1 << 32
        self.__MAX_INT64 = 1 << 64
        self._switch = {
            'DATETIME'  : self.__check_date,
            'INT'       : self.__check_int,
            'INT64'     : self.__check_int64,
            'BIGINT'    : self.__check_int64,
            'UINT64'    : self.__check_uint64,
            'FLOAT'     : self.__check_float,
            'STRING'    : self.__check_string,
        }


    def __check_int(self, value = None, size = None):
        try:
            number = int(value)
            if (number >= self.__MAX_INT):
                raise Exception("overflower 32 bits")
        except Exception as e:
            return "TypeError:{0} {1}".format(self.value, e)

        return None

    def __check_int64(self, value = None, size = None):
        try:
            number = int(value)
            if (number >= self.__MAX_INT64):
                raise Exception("overflower 64 bits")
        except Exception as e:
            return "TypeError:{0} {1}".format(self.value, e)

        return None

    def __check_uint64(self, value = None, size = None):
        try:
            number = int(value)
            if (number < 0):
                raise Exception("{} less then 0".format(value))
            if (number >= self.__MAX_INT64):
                raise Exception("overflower 64 bits")
        except Exception as e:
            return "TypeError:{0} {1}".format(self.value, e)

        return None

    def __check_float(self, value = None, size = None):
        try:
            number = float(value)
        except Exception as e:
            return "TypeError:{0} {1}".format(self.value, e)

        return None

    def __check_string(self, value = None, size = 0):
        try:
            if (len(value) > size):
                raise Exception("overflower {} bytes".format(size))
        except Exception as e:
            return "TypeError:{0} {1}".format(self.value, e)

        return None
    def __check_date(self, value = None, size = None):
        return None

    def check_type(self, value = None, size = 0):
        return self._switch[self.name](value, size)

    def print_str(self):
        print(self.__str__())

class Field(object):
    def __init__(self, name, type, size = 0, values = [], desc = None):
        self.__name = name
        self.__type = FieldType(type)
        self.__size = size
        self.__values = values
        self.__desc = desc

    def __str__(self):
        return "{0}.{1}.[field:{2}, size:{3}, values:{4}, desc:{5}]".format(self.__class__.__name__, self.__name, self.__type, self.__size, str(self.__values), self.__desc)

    def get_name(self):
        return self.__name

    def check_field(self, value):
        result = self.__type.check_type(value, self.__size)
        if result:
            return result

        if len(self.__values) == 0:
            return None

        if not value in self.__values:
            return 'ValueError: {0} value({1}) not in {2}'.format(self.__name, value, str(self.__values))


class LogType(object):
    def __init__(self, name=None, desc=None):
        # assert(name != None, "LogType's name cannot be none")
        self.__name = name
        self.__desc = desc
        self._field = []

    def __str__(self):
        return '({0}, {1})'.format(self.__name, self.__desc)


class CheckLogMgr(object):
    def __init__(self, xml_path=None, tlog_path=None):
        if (not os.path.isfile(xml_path) or not os.path.exists(tlog_path)):
            raise Exception('Error: {0} or {1} not exists'.format(xml_path, tlog_path))

        self.__xml = xml_path
        self.__tlog = []
        self.__tree = ET.parse(self.__xml)
        self.__macros = {}
        self.__structs = {}
        self.__records = {
            'sucess':set(),         # 成功
            'missing': set(),       # xml 缺失 struct
            'column': {},           # 列数 不正确 struct
            'field': {}             # 每列类型 字段 大小 检查错误
        }

        if os.path.isfile(tlog_path):
            self.__tlog.append(tlog_path)
            return
        if not os.path.isdir(tlog_path):
            raise Exception('Error: {0} unknown file'.format(xml_path, tlog_path))
        files = os.listdir(tlog_path)

        self.__tlog = [os.path.join(tlog_path, fn) for fn in files if os.path.isfile(os.path.join(tlog_path, fn))]
        self.__parse()

    def __parse(self):
        root = self.__tree.getroot()
        if (root.tag != 'metalib' or root.attrib.get('name', '') != 'Log'):
            raise Exception("Error: {0} format invalid, root's tag{1}, root's name {2}".format(self.__xml, root.tag, root.attrib.get('name', '')))

        structs = root.findall('struct')
        macros = root.findall('macrosgroup')
        macro_idx = 0
        for macro in macros:
            macro_idx += 1
            self.__parse_macros(macro, macro_idx)

        struct_idx = 0
        for struct in structs:
            struct_idx += 1
            self.__parse_struct(struct, struct_idx)


    def __parse_struct(self, struct, struct_idx):
        if not isinstance(struct, ET.Element):
            return

        name = struct.attrib.get('name')
        if not name:
            raise Exception("Error: {0} format invalid, (idx:{1})struct's attribute name not exists".format(self.__xml, struct_idx))

        self.__structs[name] = []
        fields = self.__structs[name]
        entrys = struct.findall('entry')
        for entry in entrys:
            entry_name = entry.attrib.get('name', '')
            entry_type = entry.attrib.get('type')
            entry_size = entry.attrib.get('size', 0)
            entry_desc = entry.attrib.get('desc')
            entry_values = []
            if entry_name.upper() in self.__macros:
                entry_values = tuple(self.__macros[entry_name.upper()])

            try:
                field = Field(entry_name, entry_type, int(entry_size), entry_values, entry_desc)
            except Exception as e:
                raise Exception('TypeError: undefined type {0}, {1}'.format(entry_type, e))

            fields.append(field)

    def __parse_macros(self, macro, macro_idx):
        if not isinstance(macro, ET.Element):
            return

        name = macro.attrib.get('name')
        if not name:
            raise Exception("Error: {0} format invalid, (idx:{1})macro's attribute name not exists".format(self.__xml, macro_idx))

        self.__macros[name.upper()] = []
        values = self.__macros[name.upper()]
        defs = macro.findall('macro')
        for define in defs:
            value = define.attrib.get('value')
            if not value:
                raise Exception("Error: (idx:{1})macro({0})'s attribute value not exist".format(define.attrib.get('name', ''), macro_idx))

            values.append(value)

    def check_log(self):
        self.__records = {
            'sucess':set(),
            'missing': set(),
            'column': {},
            'field': {}
        }

        for fn in self.__tlog:
            # with open(fn, 'r', encoding='UTF-8') as f:
            with OPEN(fn, 'r', encoding='utf8') as f:
                lines = f.readlines()
                self.__check_lines(lines)

    def __check_lines(self, lines):
        if not isinstance(lines, list):
            return

        for line in lines:
            fields = line.split('|')
            name = fields[0]
            fields = fields[1:]

            if not name in self.__structs:
                self.__records['missing'].add(name)
                continue;

            struct = self.__structs[name]
            if len(fields) != len(struct):
                self.__records['column'][name] = '{0} request {1} columns, but {2} columns'.format(name, len(struct), len(fields))
                continue;

            is_sucess = True
            for i in range(0, len(struct)):
                result = struct[i].check_field(fields[i])
                if result:
                    error = self.__records['field'].get(name, {})
                    error[struct[i].get_name()] = result + "-->line:" + line
                    self.__records['field'][name] = error
                    is_sucess = False

            if is_sucess:
                self.__records['sucess'].add(name)

    def print_log(self, output = None):
        # for struct in self.__structs:
            # for field in self.__structs[struct]:
                # print("{0} {1}".format(struct, str(field)))
        # print(str(self.__macros))

        for struct in self.__records['missing']:
            self.__records['sucess'].discard(struct)
        for struct in self.__records['field']:
            self.__records['sucess'].discard(struct)
        for struct in self.__records['column']:
            self.__records['sucess'].discard(struct)
        log_miss = set()
        for i in self.__structs:
            if not i in self.__records['sucess']:
                log_miss.add(i)

        field_error = ''
        for struct in self.__records['field']:
            for field in self.__records['field'][struct]:
                field_error += struct + ' --> ' + field + ' : ' + self.__records['field'][struct][field] + '\n'

        column_error = ''
        for struct in self.__records['column']:
            column_error += struct + ' --> ' + self.__records['column'][struct] + '\n'

        sucess = '\n'.join(self.__records['sucess'])
        xml_miss = '\n'.join(self.__records['missing'])
        log_miss = '\n'.join(log_miss)


        log = u"--------------------Note------------------------\n" +\
              u'sucess: sucess log struct\n' +\
              u"column_error: the number of column is different of xml request\n" +\
              u"field_error: the field of struct check error\n" +\
              u"xml_miss: the struct missing in xml\n" +\
              u"log_miss: the struct missing in tlog\n" +\
              '\n------------column_error-----------\n' +   \
              column_error + '\n' + \
              '\n------------field_error-----------\n' +    \
              field_error  + '\n' +  \
              '\n------------sucess-----------\n' + \
              sucess + '\n' +   \
              '\n------------xml_miss-----------\n' +   \
              xml_miss + '\n' +  \
              '\n------------log_miss-----------\n' +   \
              log_miss

        try:
            with OPEN(output, 'w', encoding='utf8') as f:
                f.write(log)
        except Exception as e:
            print('Warn:output log cannot write ', e)
        finally:
                print(log.encode('utf-8'))

def test():
    mgr = CheckLogMgr('./tlog.xml', './log');
    mgr.check_log()
    mgr.print_log()

if __name__ == '__main__':
    test()
